package com.gearbox.core.util;

import com.gearbox.core.model.shell.ShellResult;

import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class ShellExecutor {
    private static final Logger LOG = LoggerFactory.getLogger(ShellExecutor.class);

    public static final int TIMEOUT_EXIT_CODE = 13;

    public static final int ERROR_EXIT_CODE = 1;

    private static final int DEFAULT_WAIT_TIME = 10000;


    public static ShellResult runCommand(String command) {
        ShellResult result = runCommand(command, new Object[0], DEFAULT_WAIT_TIME);
        LOG.info("run command result is {}", result);
        return result;
    }

    public static ShellResult runCommand(
        String command, Object[] args, int timeoutMs
    ) {
        String[] commands = buildCommands(SystemUtils.IS_OS_WINDOWS
            ? buildWindowsCommands(command) : buildLinuxCommands(command), args);
        Process process = null;
        ShellResult result = new ShellResult();
        result.setCommandLine(command);
        try {
            /**
             * 这个地方子进程不能开启重定向，以windows为例，如果开启了重定向，timeout命令就无法成功。 会约束被启动进程的文件描述符使用
             */
            process = new ProcessBuilder(commands).start();
            process.getOutputStream().close();
            if (!process.waitFor(timeoutMs, TimeUnit.MILLISECONDS)) {
                LOG.error("runCommand timeout, command is:{}", Arrays.toString(commands));
                result.setExitValue(TIMEOUT_EXIT_CODE);
                return result;
            } else {
                int exitCode = process.exitValue();
                List<String> response = readTextLimited(process.getInputStream());
                if (response.isEmpty()) {
                    response = readTextLimited(process.getErrorStream());
                }
                result.setExitValue(exitCode);
                result.setReturnValues(response);
                return result;
            }
        } catch (Exception e) {
            LOG.error("runCommand error, command is:{}", Arrays.toString(commands), e);
            result.setExitValue(ERROR_EXIT_CODE);
            return result;
        } finally {
            if (process != null) {
                process.destroyForcibly();
            }
        }
    }

    private static String[] buildCommands(String[] cmds, Object[] args) {
        String[] result = new String[cmds.length + args.length];
        System.arraycopy(cmds, 0, result, 0, cmds.length);
        for (int i = 0; i < args.length; ++i) {
            result[cmds.length + i] = args[i] == null ? "" : String.valueOf(args[i]);
        }
        return result;
    }

    private static String cleanPathOnWindows(String file) {
        String escaped = file.replace('/', '\\');
        if (escaped.charAt(0) == '\\') {
            return escaped.substring(1);
        } else {
            return escaped;
        }
    }

    /**
     * windows 执行命令需要前置"cmd"信息
     *
     * @param command
     * @return
     */
    private static String[] buildWindowsCommands(String command) {
        return new String[] {"cmd", "/c", cleanPathOnWindows(command)};
    }

    /**
     * 针对不可执行命令，使用sh去执行
     *
     * @param command
     * @return
     */
    private static String[] buildLinuxCommands(String command) {
        return new String[] {"/bin/sh", "-c", command};
    }

    public static List<String> readTextLimited(InputStream stream) {
        try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(stream, StandardCharsets.UTF_8))) {
            if (reader.ready()) {
                String line;
                List<String> result = new ArrayList<>();
                while ((line = reader.readLine()) != null) {
                    result.add(line);
                }
                return result;
            }
            return new ArrayList<>();
        } catch (IOException e) {
            LOG.error("read input stream error", e);
            return new ArrayList<>();
        } finally {
            try {
                stream.close();
            } catch (IOException e) {
                LOG.error("readStreamLimited close stream error", e);
            }
        }
    }
}
